package tests

case class SimpleDescription(value: String) extends Description {
  override def toString = value
}

object BlockStatement {
  type CodeBlock = ()=> Any
  val DoNothing: CodeBlock = () => {}
}

abstract class BlockStatement[A <: Statement](description: String) extends Statement {

  import BlockStatement._

  protected var codeBlock: CodeBlock = DoNothing

  def apply(block: => Any): A = {
    codeBlock = () => { block }
    this.asInstanceOf[A]
  }

  def apply = {
    codeBlock()
  }

  def describe = SimpleDescription(description)

}

case class BeforeStatement(description: String) extends BlockStatement[BeforeStatement](description)

case class AfterStatement(description: String) extends BlockStatement[AfterStatement](description)

class TestStatement(var description: String) extends BlockStatement[TestStatement](description) with Test {

  import scala.collection.mutable.ListBuffer

  private var beforesList = new ListBuffer[Statement]
  private var aftersList = new ListBuffer[Statement]
  private var skipped = false
  private var skipReasonDesc: String = null

  final def executeTest = {
    if (!wasSkipped)
      execute
  }

  def execute = codeBlock()

  def befores = beforesList.readOnly

  def afters = aftersList.readOnly

  def addBefore(before: Statement) = beforesList + before

  def addAfter(after: Statement) = aftersList + after

  def skip: TestStatement = {
    skip("skipped test")
  }

  def skip(reason: String): TestStatement = {
    skipReasonDesc = reason
    skipped = true
    this
  }

  def skip(reason: TestStatement): TestStatement = {
    skip(description)
    description = reason.description
    this
  }

  def wasSkipped = skipped

  def skipReason = skipReasonDesc

  def timeout(timeout: Long) = TimedOutTestStatement(description, timeout)

  def throws(expected: Class[_ <: Throwable]) = ExpectExceptionStatement(description, expected)

}

case class TimedOutTestStatement(desc: String, timeout: Long) extends TestStatement(desc) {

  import java.util.concurrent.{Callable, ExecutionException, ExecutorService, 
                               Executors, Future, TimeUnit, TimeoutException}

  private var timeunit = TimeUnit.MILLISECONDS

  override def execute = {
    val service = Executors.newSingleThreadExecutor
    val callable: Callable[Any] = () => {codeBlock()}
    val result = service submit callable
    service.shutdown();
    try {
      val terminated= service.awaitTermination(timeout, timeunit)
      if (!terminated) {
        service.shutdownNow
      }
      result.get(0, timeunit)
    } catch {
      case e: TimeoutException => error("test timed out after " + timeout + " milliseconds")
      case e: ExecutionException => throw unwrap(e)
    }
  }

  implicit def block2Callable(block: => Any) = new Callable[Any] {
    def call: Any =
      try {
        block
      } catch {
        case e: Throwable => new ExecutionException(e)
      }
  }

  private def unwrap(e: Throwable): Throwable =
    if (e.getClass.isAssignableFrom(classOf[ExecutionException])) unwrap(e.getCause()) else e

  def nanoseconds = {
    timeunit = TimeUnit.NANOSECONDS
    this
  }

  def microseconds = {
    timeunit = TimeUnit.MICROSECONDS
    this
  }

  def milliseconds = {
    timeunit = TimeUnit.MILLISECONDS
    this
  }

  def seconds = {
    timeunit = TimeUnit.SECONDS
    this
  }

  def minutes = {
    timeunit = TimeUnit.MINUTES
    this
  }

  def hours = {
    timeunit = TimeUnit.HOURS
    this
  }

  def ns = nanoseconds
  def ys = microseconds
  def ms = milliseconds
  def s = seconds
  def min = minutes
  def h = hours

}

case class ExpectExceptionStatement(desc: String, expected: Class[_ <: Throwable]) extends TestStatement(desc) {

  override def execute = {
    var complete = false
    try {
      codeBlock()
      complete = true

    } catch {
      case e: Throwable =>
        if (!expected.isAssignableFrom(e.getClass)) {
          val message = "Unexpected exception, expected<" + expected.getName + 
                          "> but was<" + e.getClass.getName + ">"
          throw new Exception(message, e)
        }
    }
    if (complete)
      throw new AssertionError("Expected exception: " + expected.getName)
  }

}
